Native MafiaNet replication (ReplicaManager3 + RPC4 + DeltaSerializer)#201
Native MafiaNet replication (ReplicaManager3 + RPC4 + DeltaSerializer)#201Segfaultd wants to merge 83 commits into
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughMajor refactor replacing Flecs world and legacy messages with a Replica3-based replication system and RPC4 payload dispatch. Client/server flows now use RPC payload structs for identity/resources/chat, integrate ReadyEvent, and move scripting/resources to be world-independent with replication-driven entity ownership. ChangesReplication and runtime refactor
Sequence Diagram(s)sequenceDiagram
participant Client
participant Server
participant RPC4
participant ReadyEvent
participant ReplMgr as ReplicationManager
Client->>Server: Connect + TwoWayAuth
Server-->>Client: Auth OK + ServerResources(readyEventId,tickRate)
Client->>ReadyEvent: Add(guid), Set(readyEventId)
ReadyEvent-->>Client: ALL_SET(eventId)
Client->>ReplMgr: Init(peer, idMgr, rpc, isServer=false)
Server->>ReplMgr: PushReplicationConnection(guid), Init(isServer=true)
RPC4-->>Client: EmitLuaEvent/ChatMessage
Client-->>RPC4: ClientIdentity/ChatMessage
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
623f0b2 to
c0d2dee
Compare
Replace the flecs+BitStream entity-sync layer with native MafiaNet replication under networking/replication/: - NetworkEntity : Replica3 owns its state directly (no ECS) and serializes per-tick updates through VariableDeltaSerializer (the documented ReplicaManager3 delta path) instead of a whole-object memcmp. Construction sends a full snapshot; Serialize() returns RM3SR_SERIALIZED_UNIQUELY and only changed variables go on the wire. - ReplicationManager (a ReplicaManager3) owns the entity set, the per-connection viewer map and a configurable GridSectorizer interest index. DestroyEntity only clears a viewer mapping for actual viewer entities (owned non-viewer entities share the owner GUID). The grid inserts a small box per entity (GridSectorizer asserts on zero area) and is sized for a 20km map by default, configurable via ConfigureGrid. - ReplicationConnection drives QUERY_CONNECTION_FOR_REPLICA_LIST relevance. - EntityFactory maps a CRC32 type id to a constructor for both peers. Authority (C1) is enforced by a custom QuerySerialization keyed on ownerGUID plus a stale-owner deserialize gate. Deletes the game_sync/* streamer messages. @
Remove the IRPC/IGameRPC class hierarchy and the per-type RPC4 slot trampoline (Rpc4Slot<T>) that bridged capturing handlers onto RPC4. RPCs are now plain payload structs (a stable kIdentifier + a symmetric Serialize) dispatched by RPC4 to ordinary C function handlers — the only shape RPC4 accepts. Helpers live in networking/rpc/rpc.h (Register/Read/Broadcast/SendTo). There is no separate "game RPC": an entity-scoped call simply carries a NetworkID field the handler resolves through the ReplicationManager. Identifiers are explicit namespaced literals, not typeid (stable across compilers/binaries). NetworkPeer keeps the RPC4/NetworkIDManager/StatisticsHistory/Replication subsystems and exposes GetRPC(); the typed RegisterRPC/SendRPC/SendGameRPC surface is gone. NetworkServer gains BroadcastRPCExcept for the broadcast-to-all-but-one case (RPC4::Signal has no exclusion param). Drops the dead INTERNAL_RPC dispatcher id and the GAME_SYNC_* message ids (entity sync is native now); client_connection_finalized no longer carries the server entity id. @
The world engine no longer runs a flecs streaming world. Engine/Server/ ClientEngine are thin facades over ReplicationManager: entities are NetworkEntity*, with CreateEntity(typeId)/RemoveEntity/SetOwner/GetOwner/ GetEntityByNetworkID. The flecs world is retained only for the scripting resource tree. Removes the streaming systems (StreamEntities/AssignEntityOwnership/ TickRateRegulator/...), the Streamable/Streamer/Transform/ServerID components and modules_impl.cpp, the SetTransform/SetFrame game RPCs, and the dead client entity-destroy callback (override NetworkEntity instead). Send macros are now native and global: FW_BROADCAST_RPC / FW_SEND_RPC_TO (engine.h) and FW_SERVER_BROADCAST_RPC_EXCEPT (server.h); the entity-scoped *_GAME_RPC macros are gone. Player/StreamingFactory just stamp ownership (and viewer, server-side); nickname/hwid belong on the game entity now. @
Server: the player avatar is created by the game in its onPlayerConnect handler (which now receives just the GUID) and registered as the viewer; disconnect clears the viewer mapping and lets ReplicaManager3 tear down the player entities. ClientConnectionFinalized no longer carries an entity id. Client: the local avatar arrives via replication (recognised by ownerGUID == myGUID); the connection-finalized callback only carries the tick rate. EmitLuaEvent is now a native RPC payload handled by a plain RPC4 function that reaches the scripting engine via CoreModules. CMake: build the networking/replication sources; drop modules_impl.cpp. @
Drop the FW_*_RPC send macros in favour of typed wrapper methods: NetworkPeer::RegisterRPC<T>/BroadcastRPC<T>/SendRPC<T> and NetworkServer::BroadcastRPCExcept<T>. rpc.h keeps only the Read<T> decode helper for handlers. Reword comments that narrated the migration into plain descriptions of the current code. @
ReplicaManager3 ran at its default 30ms autoserialize interval while the server advertised ServerConfig::tickInterval (60Hz default) to clients. Apply the configured rate via SetAutoSerializeInterval: the server uses cfg.tickInterval, the client uses the rate it receives at OnConnect. Drop the now write-only _cfg member. @
Make NetworkServer::SignalExcept public so games can relay a raw RPC4 bitstream to all systems except the originator (the server-authoritative event relay), and drop the unused typed BroadcastRPCExcept<T> wrapper.
Add scripting/builtins/property.h with RegisterProperty / RegisterReadonlyProperty (scalars + strings) and RegisterObjectProperty / RegisterReadonlyObjectProperty (v8pp-wrapped values like Color/Vector3). The getter/setter are template parameters so V8's accessor callbacks stay non-capturing, replacing the per-class boilerplate (and macros) that builtins were each duplicating.
The owner is authoritative over a replicated entity's transform and the server withholds serialize updates to it, so the server cannot relocate an owned entity (teleport) through normal replication. Add a built-in mechanism: - NetworkEntity::ForceTransform() (server) pushes the entity's current position/rotation to its owner via a built-in Framework::ForceTransform RPC4 call; no-op for unowned entities. - NetworkEntity::OnTransformForced() (client) is invoked after the framework applies the received transform, so games apply it to their engine (teleport, world preload, ...). ReplicationManager::Init takes the RPC4 and registers the handler; it exposes ForceTransform(entity).
Generalize the transform-only ForceTransform into entity-defined forced state: WriteForcedState/ReadForcedState/OnStateForced and ForceState, so the server can override any owned entity (the server always has the last word) while the owner stays authoritative for its own streaming. Assign small sequential NetworkIDs on the server instead of RakNet's random 64-bit ones, which exceed JavaScript's 2^53 exact-integer range and corrupt entity ids when scripts read them. Register RPC handlers as RPC4 slots so Signal() actually dispatches them; RegisterFunction handlers are only reached by Call().
Add header-only Framework::Scripting::Builtins::Entity, a reusable replicated-entity scripting handle (NetworkID resolved via the world engine, with a server-authoritative position/rotation routed through ForceState) that mods can derive from. Register read/write properties with SetAccessorProperty instead of SetNativeDataProperty: a native-data-property setter installed on a prototype is never invoked when a script assigns to the property on an instance, so the write was silently dropped.
Add NetworkEntity::SetOwner / ReplicationManager::SetOwner: set the owner and Signal a built-in grant RPC straight to the new owner. The server withholds serialize to an entity's owner, so the grant cannot ride normal replication; other peers and a revoked previous owner still learn it through serialize, and the Deserialize authority gate rejects stale-owner uploads during the handover. ServerEngine::SetOwner routes through it. Lets a client become authoritative for an entity it gains after construction (e.g. a vehicle it drives).
Describe what the scripting/networking helpers do instead of justifying them against the previous or alternative API.
Provide a reusable chat feature shared by all mods: a ChatMessage RPC payload, a global JS Chat builtin (sendToAll / sendToPlayer over the base Entity handle), server-side receive/parse/dispatch exposed via chat callbacks, and client-side send plus a received-message callback. Mods keep only their own UI and a thin bridge from the chat callbacks to their scripting events. @
MafiaNet v0.6.0 adds a per-registration void* context to RegisterSlot, so handlers no longer need a file-static instance pointer to find their owner. RegisterRPC<T> now takes a decoded-payload callable, keeps it alive for the peer's lifetime, and dispatches it via a single trampoline that recovers the handler from the slot context. The replication and chat handlers capture their instance directly; the g_manager, g_chatInstance and g_chatClientInstance globals are gone.
SignalExcept iterated the connection list to skip the sender; RPC4 Signal already treats the system identifier as the peer to exclude when broadcasting, so one call does it. Also drops a const_cast on the already-const GetReplicaCount in ForEachEntity (the one on the non-const GetReplicaAtIndex stays until MafiaNet const-qualifies it).
MafiaNet v0.6.1 const-qualifies ReplicaManager3::GetReplicaAtIndex, so ForEachEntity can iterate replicas without casting away const.
MafiaNet's BitStream has first-class std::string Write/Read overloads using the same length-prefixed wire format as RakString, so the manual write/read branch collapses to a single symmetric Serialize call. Wire format is unchanged.
3a59a5d to
fb22bfd
Compare
Derive NetworkEntity from VirtualWorldReplica3 instead of Replica3 so the dimension is owned by the base (Get/SetVirtualWorld) rather than a redundant field. The two topology query overrides become the base's QueryConstructionWithinWorld / QuerySerializationWithinWorld hooks. Streaming now filters with VirtualWorldsCanSee, gaining the VIRTUAL_WORLD_GLOBAL "visible everywhere" sentinel the old equality check could not express. QueryReplicaList syncs the connection's world to its avatar so the construction filter and the base serialize-path filter agree. virtualWorld is dropped from the construction snapshot as server-only streaming metadata.
Entity replication is fully native (ReplicaManager3 / NetworkEntity) and the scripting Entity builtin wraps NetworkEntity by id, so the flecs world only backed the resource tree -- which duplicated ResourceManager's own std::map registry. Drop flecs entirely from the framework: - Engine: remove the flecs::world, GetWorld(), and the progress() tick; it is now purely the replication facade. - Resource/ResourceManager: drop _rootEntity, OwnedResource, GetRootEntity, DestroyChildEntities, child_of and the flecs::world* ctor params. The existing std::map registry is the tree. - Delete the empty Base/Mod modules, the dead logging/formatters.h, the module import calls and server InitModules(); remove the dead _weatherManager member and stale flecs includes. - Drop flecs_static from the framework link. - Move the resource tests off the flecs::world fixture. Breaking change: Engine no longer exposes an ECS world. MafiaMP is unaffected; other mods migrate later.
Post-flecs the World::Engine / ServerEngine / ClientEngine hierarchy owned nothing -- it was a pure forwarder to the ReplicationManager, which is owned by the NetworkPeer. Remove the layer entirely and promote the ReplicationManager to the top-level networked-world object. - CoreModules: replace Get/SetWorldEngine (World::Engine*) with Get/SetReplication (ReplicationManager*), set from the peer on server init / client connect and cleared on shutdown / disconnect. - Delete world/engine, world/server, world/client and world/errors. - Entity CRUD, ownership and the auto-serialize tick rate now go straight to the ReplicationManager; peer-lifecycle wiring moved to the integration Instances. - Scripting modules no longer take a world-engine pointer. Breaking change. MafiaMP is updated in lockstep; other mods migrate later.
The 5.7 MB prebuilt static lib was committed by mistake in f8e0882. Remove it and gitignore the services/lib Debug/Release output dirs so it cannot be re-added.
App::OnContextCreated was defined but never declared, and the App class never overrode GetRenderProcessHandler despite inheriting CefRenderProcessHandler (C2509). Declare both, and compile renderer_app.cpp into FrameworkClient so CallEventHandler::Execute resolves at link time.
Replace the mirrored Write/Read pairs on NetworkEntity (construction snapshot, forced state, construction extension hook) with single Serialize(bs, write) functions over BitStream::Serialize, matching the RPC layer. Per-tick fields now flow through a FieldSerializer adapter so the same Field(member) call serializes and deserializes, removing the silent field-order desync footgun for subclasses.
Self-registration helper for NetworkEntity subclasses, mirroring MafiaNet::RPC4GlobalRegistration. One line ties a subclass to its wire name/type id, removing the forgot-to-register and name-mismatch footguns.
Move the visible() relevance predicate and candidate gathering out of ReplicationConnection into InterestGrid::CollectVisible, so the server's relevance rule lives in one place. QueryReplicaList now just diffs the relevant set against already-constructed replicas. Drops the EntitiesOwnedBy/AlwaysVisibleEntities/QueryRadius pass-throughs on ReplicationManager in favour of a single CollectInterest facade.
ReplicationManager::Init now takes its owning NetworkPeer and routes the ForceState/SetOwner pushes through the peer's RPC helpers instead of poking RPC4 directly. SetOwner uses a typed SetOwnerRPC payload; ForceState uses a new RegisterRawRPC/SendRawRPC pair since its tail is polymorphic (resolve the entity by id, then let it deserialize). Handlers now live on the peer for its lifetime, removing the manual UnregisterSlot bookkeeping.
The type both registers entity types and constructs them; "registry" is the more honest noun. Renames the class and its files, and documents that CreateEntity returns a non-owning pointer (ReplicaManager3 owns and deletes the entity).
Register<T>(name) already covers one-line registration without a macro.
The Replica3/VirtualWorldReplica3 plumbing overrides are framework-owned; games extend NetworkEntity through the virtual hooks (SerializeFields, OnSerializeConstruction, OnConstructed, SerializeForcedState, OnStateForced), never by reimplementing the wire/authority methods. Mark those overrides final so the compiler enforces it, and return the *WithinWorld hooks to protected to match VirtualWorldReplica3 rather than widening their access.
NetworkID is a bare typedef uint64_t, so peer guids and entity ids were the same type to the compiler. Wrap a peer guid in enum class PeerGuid (trivially copyable -> identical on the wire) and use it across the owner/viewer/disconnect surface, with ToPeerGuid/ToGuid at the MafiaNet boundary.
…ative-replication # Conflicts: # vendors/mafianet/CMakeLists.txt # vendors/mafianet/README.md # vendors/mafianet/Source/include/mafianet/version.h # vendors/mafianet/VERSION.txt
Drop the homemade PeerGuid enum/helpers in favour of MafiaNet::PeerGuid (vendored v0.9.0) and carry it through the owner/viewer/disconnect surface, removing the uint64_t casts at the boundaries.
The per-tick VariableDeltaSerializer path only carries changes against a shared baseline, so a late-constructing connection never received a field that was not actively changing (a parked car's colour, an idle player's health). Run SerializeFields once verbatim in the construction snapshot via a plain FieldSerializer backend to seed the full current state.
On disconnect, return any non-viewer entity the peer owned (e.g. a vehicle it was driving) to the server; otherwise the authority gate freezes it against an owner that no longer exists.
The exact-hash build-token handshake replaced the semver-range version gate; nothing calls VersionSatisfies.
The vendored GridSectorizer compiles without removal support, so InterestGrid::Remove could not take an entity out of the spatial grid: a DestroyEntity issued mid receive-loop (a disconnect handler, an RPC) left a dangling pointer that the very next QueryRadius dereferenced — a use-after-free on any ordinary disconnect with players online. Track the indexed entities in a live set that Remove() updates and queries filter through, making the stale grid entry harmless. Bump a generation counter on every index change and cache each connection's interest result against it: ReplicaManager3 re-runs QueryReplicaList on every RakPeer::Receive() call, but the index changes once per tick, so the recompute (and its per-call set/list allocations, now reused members) happens only when the generation or the viewer changed. Removal bumps the generation too, which evicts destroyed entities from the per-connection caches. Also drop the always-true range-set membership test from the in-range visibility loop and address Remove's owned-bucket by ownerGUID instead of sweeping every bucket, keeping the sweep as a fallback.
DestroyEntity cleared the viewer mapping by the entity's current ownerGUID without checking what the mapping pointed at. On the documented respawn flow — SetViewer(guid, newAvatar) then DestroyEntity(oldAvatar) — that erased the replacement's mapping and silenced the connection's streaming for good; with an owner reassigned before the destroy it cleared the wrong key and left a dangling viewer pointer behind. Erase by value instead. SetViewer now also clears the previous avatar's isViewer flag (nothing ever reset it), and ClearViewer resets the flag on the mapping it removes.
OnClosedConnection is a plugin callback and fires for every closed connection, including peers dropped before they completed identity (build-token mismatch, quit during the asset phase). Those peers never produced a player-connect notification, so forwarding their teardown to the game broke the connect/disconnect pairing the old flow guaranteed — a mod resolving the guid in its disconnect handler hit a missing entry. Gate the notification on the replication connection that PushReplicationConnection creates in the identity handler, right after the connect notification fires.
A server-side override of an owned entity (ForceState, e.g. a script teleport) raced the owner's in-flight updates: for a round trip the owner's pre-override packets passed the authority gate, reverted the forced transform, and were re-broadcast to every viewer — visible rubber-banding on each teleport. Fence it with a state epoch: the server bumps it when sending a forced state, the owner adopts it from the ForceState/SetOwner RPCs and echoes it ahead of every update, and the server drops owner updates still carrying the old value. The epoch rides as a raw prefix, not a delta variable — VDS omits unchanged variables, which would let a pre-override packet pass the staleness check by absence. Equality- checked, so uint8 wrap is harmless. ForceState also gains the server-only gate SetOwner already had: the scripting builtins call it from shared code, and on a client it used to emit an RPC misaddressed to a peer it isn't connected to, silently dropped by RakNet. While in there, collapse the base field set (owner + transform) that construction, Serialize, and Deserialize each enumerated by hand into one SerializeBaseFields helper, so the three wire paths cannot drift.
RegisterRPC<T> duplicated the slot allocation, ownership, and RegisterSlot plumbing of RegisterRawRPC three declarations below; any change to slot lifetime or dispatch had to land twice. The typed variant is now a decode wrapper over the raw one.
The AddPassword fallback cleared the whole TwoWayAuthentication plugin whenever the identifier already existed — including outstanding challenge state, so a second SetBuildToken call (the reconnect/reload path the comment advertises) failed peers mid-handshake with a phantom build mismatch. Track the registered token and skip the re-add when it is unchanged; only a genuine token change pays the Clear(), where dropping challenges against the old token is the intended outcome.
ReplicationManager::ConfigureGrid existed but nothing called it, so every game ran on the grid's hardcoded defaults (100m cells, ±10km bounds) with no escape hatch: maps larger than the bounds silently clamp entities into edge cells and degrade border interest queries. Expose the extent in WorldConfig next to the tick interval and apply it during server init.
The branch removed the last call sites of the instance-owned PlayerFactory/StreamingFactory; the members, getters, and world/types includes survived in both integration headers, suggesting a connect flow that no longer exists. Game code that wants the archetype factories constructs its own (they are stateless).
The client finalized on any ID_READY_EVENT_ALL_SET without comparing the packet's event id to the one the server assigned in ServerResources, and nothing prevented finalization from running twice. No in-tree flow produces a stray completion today, but the mod-level spawn logic hanging off it is not safe to re-run, so check the id and latch the finalization until the connection drops.
Console commands split on getline(' ') — keeping empty tokens for
repeated spaces — while chat commands hand-rolled an istringstream loop
that collapses them, so the same line parsed differently depending on
where it arrived. Hoist a single CommandProcessor::Tokenize (collapsing
whitespace, the saner of the two behaviours) and use it on both paths.
After the hash field was dropped, ServerResourceInfo became a field-identical twin of Networking::RPC::ResourceInfo, kept in sync by a manual copy loop in the ServerResources handler. Alias the scripting type to the wire type and assign the vector directly, so a future wire field cannot silently fail to reach the resource manager.
Entry::id duplicated the map key and was never read; the key is the single source of truth.
The Entity builtin hand-rolled the accessor boilerplate that property.h exists to eliminate — property.h itself had no call sites. Teach detail::Return to push 64-bit integers as JS numbers (the double cast that kept `id` hand-rolled; exact up to 2^53, which NetworkIDs respect), then register id and position through the helpers. The rotation accessor stays custom for its dual Vector3/Quaternion setter. Also use glm's component-wise vec3 degrees/radians overloads instead of converting each component by hand.
Important
MAJOR netcode break — client and server must be updated together.
TL;DR
Replaces the hand-rolled flecs + BitStream streaming layer with native MafiaNet plugins (
ReplicaManager3,RPC4,VariableDeltaSerializer) and removes flecs from the framework core entirely.migration/mafianet-replicasWhat changed
🔁 Replication core —
networking/replication/The replicated world is now the MafiaNet plugin itself, not a flecs layer on top of it.
NetworkEntity : VirtualWorldReplica3— owns its state as plain members. Per-tick updates rideVariableDeltaSerializer(a variable is sent only when it changes); construction sends a full snapshot.ReplicationManager : ReplicaManager3+NetworkIDManager+GridSectorizerare the world — it creates/destroys entities, resolves them byNetworkID, tracks each connection's viewer entity, and drives interest management.ownerGUIDviaQuerySerialization: the server serializes to everyone except the owner, the owning client serializes upstream, and deserialize accepts state only from the current owner.🗑️ Legacy world engine removed
World::EnginefacadeReplicationManager(used directly)world/server & clientgame_syncmessages,world/game_rpcCoreModules::GetWorldEngine()CoreModules::GetReplication()flecs is gone from the framework core.
📡 RPC over RPC4
Signal()and routed through RPC4's per-slot context — no file-static handler pointers.NetworkPeer:RegisterRPC/BroadcastRPC/SendRPC.🎮 Server-authoritative overrides
NetworkEntity::ForceState/WriteForcedState/OnStateForced— push state onto an owning client so the server gets the last word.SetOwner— grant ownership directly to a client (serialize to an owner is withheld, so the grant can't ride normal replication).✨ Other improvements
VirtualWorldReplica3, so dimension filtering happens natively before the topology decision.networking/rpc/chat_message.h, nativestd::string) and the scriptableChatAPI (scripting/builtins/chat.h), reusable across mods and targeting players through the baseEntityhandle.Builtins::Entitybase +property.hregistration helpers; accessors useSetAccessorProperty; integral setters accept any JS number.OnClosedConnectiondestroys a dropped peer's server-created viewer, so avatars no longer leak across reconnects.MafiaHubServices.libbuild artifact.Status & risk
Builds
Validated in-game
Not yet validated
Known open
Summary by CodeRabbit
New Features
Chores